//
// CrackCommander
//
// Copyright (C) jextra.net.
//
//  This file is part of the Crack build system.
//
//  The Crack build system is free software; you can redistribute it 
//  and/or modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  The Crack build system is distributed in the hope that it will be 
//  useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with the Crack build system; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
//  02111-1307 USA.
//

package net.jextra.crack;

import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.*;
import net.jextra.crack.core.*;

/**
 * <p>
 * Used by {@link CrackBuild} to manage commands. It should not be used directly.
 * </p>
 */
public class CrackCommander
{
    // ============================================================
    // Fields
    // ============================================================

    public static final char PACKAGE_SEPARATOR = '.';

    private int commandLevel = 0;
    private ArrayList<CommandWrapper> commandWrappers;
    private HashMap<Object, String> knobPrefixes;
    private Set<CommandWrapper> history;

    // ============================================================
    // Constructors
    // ============================================================

    public CrackCommander()
    {
        commandWrappers = new ArrayList<CommandWrapper>();
        knobPrefixes = new HashMap<Object, String>();
        history = new HashSet<CommandWrapper>();
    }

    // ============================================================
    // Methods
    // ============================================================

    // ----------
    // public
    // ----------

    // Make the next header be a "top" level header;
    public void resetCommandLevel()
    {
        commandLevel = 0;
    }

    public void clearCommandHistory()
    {
        history.clear();
    }

    public void addCommands( Object knob )
        throws SecurityException, NoSuchMethodException
    {
        addCommands( null, knob );
    }

    public void addCommands( String commandPrefix, Object knob )
        throws SecurityException, NoSuchMethodException
    {
        if ( commandPrefix == null )
        {
            commandPrefix = "";
        }

        if ( knobPrefixes.containsKey( knob ) )
        {
            throw new CrackError( "addCommands can only be called once per command Knob: "
                + knob.getClass().getCanonicalName() );
        }
        knobPrefixes.put( knob, commandPrefix );

        //
        // Find all public methods with CrackCommand annotations and add them to the commands list.
        //
        for ( Method method : knob.getClass().getMethods() )
        {
            if ( !method.isAnnotationPresent( CrackCommand.class ) )
            {
                continue;
            }
            else if ( !Modifier.isPublic( method.getModifiers() ) )
            {
                continue;
            }
            else if ( method.getParameterTypes().length > 0 )
            {
                new CrackException( String.format( "CrackCommand method '%s' can not have any " + "parameters.",
                    method.getName() ) );
            }

            CrackCommand ann = method.getAnnotation( CrackCommand.class );
            String commandName = ann.value();
            // By default the, the method name is used as the command name.
            if ( commandName.equals( CrackCommand.UNNAMED ) )
            {
                commandName = method.getName();
            }

            String userCommandName = commandPrefix + commandName;
            String fullCommandName = knob.getClass().getCanonicalName() + "#" + commandName;

            //
            // See if the command with the same name already exists, if so, throw an exception.
            //
            CommandWrapper wrapper = getCommand( userCommandName );
            if ( wrapper != null )
            {
                wrapper = getCommand( userCommandName );
                throw new CrackException( String.format( "'%s' conflicts with another command "
                    + "with the same name.\n[%s : %s]\n[%s : %s]", userCommandName, knob.getClass().getCanonicalName(),
                    method.getName(), wrapper.getCommandsKnob().getClass().getCanonicalName(), wrapper.getMethod()
                        .getName() ) );
            }
            else
            {
                //
                // Register both the hidden full name and the "pretty" name.
                //
                wrapper = new CommandWrapper( knob, method, ann );
                wrapper.addRegistration( new CommandWrapper.CommandRegistration( userCommandName, ann.visible() ) );
                wrapper.addRegistration( new CommandWrapper.CommandRegistration( fullCommandName, false ) );

                //
                // Add arguments to wrapper.
                //
                int i = 0;
                for ( Class<?> type : method.getParameterTypes() )
                {
                    CommandWrapper.CommandArg arg = new CommandWrapper.CommandArg( "arg" + i, null, type );

                    for ( Annotation pa : method.getParameterAnnotations()[i++] )
                    {
                        if ( pa instanceof CrackArg )
                        {
                            CrackArg ca = (CrackArg) pa;
                            String name = ca.value();
                            if ( name.equals( CrackArg.UNNAMED ) )
                            {
                                name = "arg" + i;
                            }

                            arg = new CommandWrapper.CommandArg( name, ca.description(), type );
                            break;
                        }
                    }

                    wrapper.addArg( arg );
                }

                commandWrappers.add( wrapper );
            }
        }
    }

    public CommandWrapper getCommand( String command )
    {
        for ( CommandWrapper wrapper : commandWrappers )
        {
            CommandWrapper.CommandRegistration reg = wrapper.getRegistration( command );
            if ( reg != null )
            {
                return wrapper;
            }
        }

        return null;
    }

    public void createCommandAlias( String existingName, String aliasName )
    {
        CommandWrapper wrapper = getCommand( existingName );

        assert wrapper != null : "'" + existingName + "' command does not exist.";
        assert getCommand( aliasName ) == null : "'" + aliasName + "' command alias already exists.";

        wrapper.addRegistration( new CommandWrapper.CommandRegistration( aliasName, true ) );
    }

    public void renameCommand( String oldName, String newName )
    {
        assert getCommand( newName ) == null : "'" + newName + "' command already exists.";
        CommandWrapper wrapper = getCommand( oldName );
        assert wrapper != null : "'" + oldName + "' command does not exist.";

        wrapper.renameRegistration( oldName, newName );
    }

    public void setCommandVisible( String name, boolean visible )
    {
        CommandWrapper wrapper = getCommand( name );
        assert wrapper != null : "'" + name + "' command does not exist.";

        wrapper.getRegistration( name ).setVisible( visible );
    }

    public boolean removeCommand( String command )
    {
        CommandWrapper wrapper = getCommand( command );
        if ( wrapper == null )
        {
            return false;

        }

        return commandWrappers.remove( wrapper );
    }

    public void invokeKnobCommand( Object knob, String command, String ... args )
        throws Exception
    {
        String prefix = knobPrefixes.get( knob );
        if ( prefix == null )
        {
            prefix = "";
        }

        invokeCommand( prefix + command, args );
    }

    public void invokeCommand( String command, String ... args )
        throws Exception
    {
        CommandWrapper wrapper = getCommand( command );
        if ( wrapper != null )
        {
            printHeader( wrapper.getName() );
            commandLevel++;

            history.add( wrapper );

            //
            // Parse arguments
            //
            if ( args.length != wrapper.getArgs().size() )
            {
                throw new CrackError( "Command requires " + wrapper.getArgs().size() + " arguments. " + args.length
                    + " were specified." );
            }

            Object[] realArgs = new Object[args.length];
            for ( int i = 0; i < args.length; i++ )
            {
                realArgs[i] = marshallArg( wrapper.getArgs().get( i ), args[i] );
            }

            //
            // Call the method.
            //
            Object retVal = null;
            try
            {
                retVal = wrapper.getMethod().invoke( wrapper.getCommandsKnob(), realArgs );
            }
            catch ( InvocationTargetException ex )
            {
                if ( ex.getCause() instanceof Exception )
                {
                    throw (Exception) ex.getCause();
                }
                else if ( ex.getCause() instanceof Error )
                {
                    throw (Error) ex.getCause();
                }
                else
                {
                    throw new CrackException( ex.getCause() );
                }
            }
            catch ( Exception ex )
            {
                System.out.println( "EXCEPTION: " + ex.getClass().getCanonicalName() );
                throw ex;
            }

            if ( retVal != null )
            {
                if ( retVal instanceof Integer )
                {
                    int retValInt = (Integer) retVal;
                    if ( retValInt != 0 )
                    {
                        throw new CrackError( "Crack command returned a non-zero int: %d", retValInt );
                    }
                }
                else if ( retVal instanceof String )
                {
                    String retValInt = (String) retVal;
                    if ( retValInt != null )
                    {
                        throw new CrackError( "Crack command returned a non-null String: '%s'", retValInt );
                    }
                }
                else
                {
                    throw new CrackError( "Unsupported Crack command return type: %s", retVal.getClass()
                        .getCanonicalName() );
                }
            }

            commandLevel--;
            printFooter();

        }
        else
        {
//            throw new CrackError( String.format( "Unknown command '%s'.", command ) );
            System.out.printf( "\nUnknown command '%s'.\n", command );
        }
    }

    public void printHelp( boolean showHidden )
        throws Exception
    {
        Crack.printHelpHeader();

        TreeMap<String, String> descriptions = new TreeMap<String, String>( new CrackCommandComparator() );
        HashMap<String, CommandWrapper> wrappers = new HashMap<String, CommandWrapper>();

        int maxLength = 0;
        // Add commands.
        for ( CommandWrapper wrapper : commandWrappers )
        {
            CommandWrapper.CommandRegistration firstReg = null;
            for ( CommandWrapper.CommandRegistration reg : wrapper.getRegistrations() )
            {
                if ( !reg.isVisible() && !showHidden )
                {
                    continue;
                }

                String description = null;
                if ( firstReg == null )
                {
                    description = wrapper.getAnnotation().description();
                    firstReg = reg;
                }
                else
                {
                    description = "See command '" + firstReg.getName() + "'";
                }

                descriptions.put( reg.getName(), description );
                wrappers.put( reg.getName(), wrapper );
                if ( reg.getName().length() > maxLength )
                {
                    maxLength = reg.getName().length();
                }
            }

            for ( CommandWrapper.CommandArg arg : wrapper.getArgs() )
            {
                if ( arg.getName().length() + 3 > maxLength )
                {
                    maxLength = arg.getName().length() + 3;
                }
            }
        }

        System.out.println( "Commands:" );
        System.out.println();
        String prevPkg = null;
        for ( String commandName : descriptions.keySet() )
        {
            String pkg = commandName.substring( 0, commandName.lastIndexOf( PACKAGE_SEPARATOR ) + 1 );
            if ( prevPkg != null && !prevPkg.equals( pkg ) )
            {
                System.out.println();
            }
            prevPkg = pkg;
            System.out.printf( "  %-" + maxLength + "s  %s\n", commandName, descriptions.get( commandName ) );

            CommandWrapper wrapper = wrappers.get( commandName );
            for ( CommandWrapper.CommandArg arg : wrapper.getArgs() )
            {
                System.out.printf( "     %-" + maxLength + "s  [%s] %s\n", arg.getName(), getArgTypeString( arg ),
                    arg.getDescription() == null ? "" : arg.getDescription() );
            }
        }
    }

    public void printHeader( String commandName )
    {
        switch ( commandLevel )
        {
            case 0:
                System.out.println();
                System.out.println( CrackAdmin.PRIMARY_HEADER );
                System.out.printf( CrackAdmin.PRIMARY_HEADER_FORMAT, commandName );
                System.out.println( CrackAdmin.PRIMARY_HEADER );
                System.out.println();
                break;

            default:
                StringBuilder builder = new StringBuilder();
                for ( int i = 47 - commandName.length() - ( 5 * commandLevel ); i > 0; i-- )
                {
                    builder.append( "-" );
                }
                System.out.printf( "- %s %s\n", commandName, builder.toString() );
                System.out.println();
        }
    }

    public void printFooter()
    {
        switch ( commandLevel )
        {
            case 0:
                break;

            default:
                StringBuilder builder = new StringBuilder();
                for ( int i = 50 - ( 5 * commandLevel ); i > 0; i-- )
                {
                    builder.append( "-" );
                }
                System.out.println();
                System.out.println( builder.toString() );
                System.out.println();
        }
    }

    // ----------
    // private
    // ----------

    private String getArgTypeString( CommandWrapper.CommandArg arg )
    {
        Class<?> t = arg.getType();

        if ( t.equals( String.class ) )
        {
            return "text";
        }
        else if ( t.equals( Boolean.class ) || t.equals( Boolean.TYPE ) )
        {
            return "boolean";
        }
        else if ( t.equals( Integer.class ) || t.equals( Integer.TYPE ) )
        {
            return "int";
        }
        else if ( t.equals( Long.class ) || t.equals( Long.TYPE ) )
        {
            return "long";
        }
        else if ( t.equals( Float.class ) || t.equals( Float.TYPE ) )
        {
            return "float";
        }
        else if ( t.equals( Double.class ) || t.equals( Double.TYPE ) )
        {
            return "double";
        }

        return "????";
    }

    private Object marshallArg( CommandWrapper.CommandArg arg, String value )
    {
        Class<?> t = arg.getType();

        try
        {
            if ( t.equals( String.class ) )
            {
                return value;
            }
            else if ( t.equals( Boolean.class ) || t.equals( Boolean.TYPE ) )
            {
                return Boolean.parseBoolean( value );
            }
            else if ( t.equals( Integer.class ) || t.equals( Integer.TYPE ) )
            {
                return Integer.parseInt( value );
            }
            else if ( t.equals( Long.class ) || t.equals( Long.TYPE ) )
            {
                return Long.parseLong( value );
            }
            else if ( t.equals( Float.class ) || t.equals( Float.TYPE ) )
            {
                return Float.parseFloat( value );
            }
            else if ( t.equals( Double.class ) || t.equals( Double.TYPE ) )
            {
                return Double.parseDouble( value );
            }
        }
        catch ( Exception ex )
        {
            throw new CrackError( "Unable to convert text '" + value + "' for argument '" + arg.getName()
                + "' into type " + getArgTypeString( arg ) + "." );
        }

        throw new CrackError( "Unknown argument type for argument '" + arg.getName() + "'" );
    }

    // ============================================================
    // Inner Classes
    // ============================================================

    /**
     * Helps sort commands by name for help command.
     */
    private class CrackCommandComparator implements Comparator<String>
    {
        @Override
        public int compare( String string1, String string2 )
        {
            int period1 = string1.lastIndexOf( PACKAGE_SEPARATOR );
            String pkg1 = string1.substring( 0, period1 + 1 );
            String name1 = string1.substring( period1 + 1 );

            int period2 = string2.lastIndexOf( PACKAGE_SEPARATOR );
            String pkg2 = string2.substring( 0, period2 + 1 );
            String name2 = string2.substring( period2 + 1 );

            if ( pkg1.equals( pkg2 ) )
            {
                return name1.compareTo( name2 );
            }

            return pkg1.compareTo( pkg2 );
        }
    }
}
